/*
 * Copyright (c) United Binary LLC.  All rights reserved.
 * 
 * This code is licensed under the MIT License
 * 
 * SEE: http://harnessit.codeplex.com/license
 * 
 */
using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Net;
using System.Web;

using UnitedBinary.HarnessIt.SharedTypes;
using UnitedBinary.UnitTests.Framework.TestItems;
using UnitedBinary.HarnessIt.TestEngine;
using UnitedBinary.Core.Diagnostics;

namespace UnitedBinary.HarnessIt
{
	internal class XmlLog
	{
		internal static void Write(string file, ReportPerformanceDetails perf, 
			TestProject project, Assembly [] assemblies, ITestEngine iTestEngine,
			HarnessItTraceListener traceListener)
		{
			XmlDocument doc = BuildXml(perf, project, assemblies, iTestEngine, traceListener);
			
			XmlTextWriter w = new XmlTextWriter(file, System.Text.Encoding.UTF8);
			w.Formatting = Formatting.Indented;

			doc.Save(w);
			w.Close();
		}

		internal static XmlDocument BuildXml(ReportPerformanceDetails perf, 
			TestProject project, Assembly [] assemblies, ITestEngine iTestEngine,
			HarnessItTraceListener traceListener)
		{
			StringBuilder sb = new StringBuilder(131072); // 128 KB initial size.

			sb.Append("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n");
			
			sb.Append("<solution name=\"");
			sb.Append(Path.GetFileNameWithoutExtension(project.Name));
			
			sb.Append("\" schemaVersion=\"1\">\n");

			//
			// Execution Settings Block
			//
			//sb.Append("<!--  -->\n");
			sb.Append("<executionSettings>\n");
						
			sb.Append("<machine name=\"");
			try
			{
				sb.Append(Environment.MachineName);
			}
			catch 
			{
				sb.Append("Unknown");
			}
			sb.Append("\" ip=\"");
			try
			{
				sb.Append(GetIPAddress());
			}
			catch 
			{
				sb.Append("Unknown");
			}
			sb.Append("\" />\n");

			sb.Append("<user name=\""); 
			sb.Append(Environment.UserName);
			sb.Append("\" />\n");

			sb.Append("<dateTime date=\"");
			sb.Append(DateTime.Now.ToShortDateString());
			sb.Append("\" time=\"");
			sb.Append(DateTime.Now.ToShortTimeString());
			sb.Append("\" />\n");

			string basedir = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
			
			UnitedBinary.HarnessIt.SharedTypes.Assembly HarnessIt = 
				new UnitedBinary.HarnessIt.SharedTypes.Assembly(basedir + "HarnessIt.exe");

			sb.Append("<version HarnessItVersion=\"");
			sb.Append(HarnessIt.Version);
			sb.Append("\" />\n");

			sb.Append("</executionSettings>\n");

			// TODO: Iterate over all projects when solution construct is in place.
			//
			// Statistics Block
			//
			int successes = 0;
			int failures = 0;
			double runningTime = 0.0;
			foreach (Assembly z in assemblies)
			{
				successes	+= z.TestRecord.Successes;
				failures	+= z.TestRecord.Failures;
				runningTime += z.RunningTime;
			}

			sb.Append("<statistics>\n");

			sb.Append("<passed>");
			sb.Append(successes);
			sb.Append("</passed>\n");

			sb.Append("<failed>");
			sb.Append(failures);
			sb.Append("</failed>\n");

			sb.Append("<total>");
			sb.Append((failures + successes));
			sb.Append("</total>\n");

			sb.Append("<runningTime>");
			sb.Append(runningTime);
			sb.Append("</runningTime>\n");

			sb.Append("</statistics>\n");


			//
			// Projects Block
			//

			// TODO: Be sure to update the project count when a solution construct is added.
			sb.Append("<projects count=\"1\">\n");
			
			sb.Append("<project name=\"");
			sb.Append(Path.GetFileNameWithoutExtension(project.Name));
			sb.Append("\">\n");

			if (project.FullPathName.Trim().Length > 0)
			{
				sb.Append("<file>");
				sb.Append(project.FullPathName.Trim());
				sb.Append("</file>\n");
			}
			else
			{
				sb.Append("<file />");
			}

			//
			// Engine Block
			//
			// sb.Append("<!-- engine block -->\n");
			sb.Append("<executionEngine>\n");
			
			sb.Append("<name>");
			sb.Append(iTestEngine.Name);
			sb.Append("</name>\n");
			
			string settings = iTestEngine.GetSettings();
			if (settings.Length > 0)
			{
				sb.Append("<settings>");
				sb.Append(settings);
				sb.Append("</settings>\n");
			}
			else
			{
				sb.Append("<settings />");
			}

			sb.Append("</executionEngine>\n");

			//
			// Statistics Block
			//
			successes = 0;
			failures = 0;
			runningTime = 0.0;
			foreach (Assembly z in assemblies)
			{
				successes	+= z.TestRecord.Successes;
				failures	+= z.TestRecord.Failures;
				runningTime += z.RunningTime;
			}

			sb.Append("<statistics>\n");

			sb.Append("<passed>");
			sb.Append(successes);
			sb.Append("</passed>\n");

			sb.Append("<failed>");
			sb.Append(failures);
			sb.Append("</failed>\n");

			sb.Append("<total>");
			sb.Append((failures + successes));
			sb.Append("</total>\n");

			sb.Append("<runningTime>");
			sb.Append(runningTime);
			sb.Append("</runningTime>\n");

			sb.Append("</statistics>\n");

			//
			// Assemblies Block
			//
			sb.Append("<assemblies count=\"");
			sb.Append(assemblies.Length);
			sb.Append("\">\n");

			foreach (Assembly A in assemblies)
			{
				//
				// Assembly Block
				//
				//sb.Append("<!-- <assembly name=\"\" [description=\"\"]> -->\n");

				bool hasRun = false;
				foreach (Class C in A.Classes)
				{
					foreach (Method m in C.Methods)
					{
						if (m.HasRun)
						{
							hasRun = true;
							break;
						}
					}
					if (hasRun)
					{
						break;
					}
				}
				
				string description = HttpUtility.HtmlEncode(A.Description.Trim());
				if (!hasRun)
				{
					sb.Append("<assembly name=\"");
					sb.Append(A.Name);
					sb.Append("\" hasRun=\"");
					sb.Append(hasRun);
					sb.Append("\">\n");

					if (description.Length == 0)
					{
						sb.Append("<description />\n");
					}
					else
					{
						sb.Append("<description>");
						sb.Append(description);
						sb.Append("</description>\n");
					}

					sb.Append("</assembly>\n");
				}
				else
				{
					sb.Append("<assembly name=\"");
					sb.Append(A.Name);
					sb.Append("\" hasRun=\"");
					sb.Append(hasRun);
					sb.Append("\">\n");

					if (description.Length == 0)
					{
						sb.Append("<description />\n");
					}
					else
					{
						sb.Append("<description>");
						sb.Append(description);
						sb.Append("</description>\n");
					}

					//
					// Statistics Block
					//
					sb.Append("<statistics>\n");

					sb.Append("<passed>");
					sb.Append(A.TestRecord.Successes);
					sb.Append("</passed>\n");

					sb.Append("<failed>");
					sb.Append(A.TestRecord.Failures);
					sb.Append("</failed>\n");

					sb.Append("<total>");
					sb.Append((A.TestRecord.Failures + A.TestRecord.Successes));
					sb.Append("</total>\n");

					sb.Append("<runningTime>");
					sb.Append(A.RunningTime);
					sb.Append("</runningTime>\n");

					sb.Append("</statistics>\n");

					//
					// Classes Block
					//
					sb.Append("<classes count=\"");
					sb.Append(A.Classes.Length);
					sb.Append("\">\n");

					foreach (Class C in A.Classes)
					{
						//
						// Class Block
						//
						//sb.Append("<!-- <class name=\"\" [description=\"\"]> -->\n");

						hasRun = false;
						foreach (Method m in C.Methods)
						{
							if (m.HasRun)
							{
								hasRun = true;
								break;
							}
						}

						description = HttpUtility.HtmlEncode(C.Description.Trim());
						if (!hasRun)
						{
							sb.Append("<class name=\"");
							sb.Append(C.Name);
							sb.Append("\" hasRun=\"");
							sb.Append(hasRun);
							sb.Append("\">\n");

							if (description.Length == 0)
							{
								sb.Append("<description />\n");
							}
							else
							{
								sb.Append("<description>");
								sb.Append(description);
								sb.Append("</description>\n");
							}

							sb.Append("</class>\n");
						}
						else
						{
							sb.Append("<class name=\"");
							sb.Append(C.Name);
							sb.Append("\" hasRun=\"");
							sb.Append(hasRun);
							sb.Append("\">\n");

							if (description.Length == 0)
							{
								sb.Append("<description />\n");
							}
							else
							{
								sb.Append("<description>");
								sb.Append(description);
								sb.Append("</description>\n");
							}

							//
							// Statistics Block
							//
							sb.Append("<statistics>\n");

							sb.Append("<passed>");
							sb.Append(C.TestRecord.Successes);
							sb.Append("</passed>\n");

							sb.Append("<failed>");
							sb.Append(C.TestRecord.Failures);
							sb.Append("</failed>\n");

							sb.Append("<total>");
							sb.Append((C.TestRecord.Failures + C.TestRecord.Successes));
							sb.Append("</total>\n");

							sb.Append("<runningTime>");
							sb.Append(C.RunningTime);
							sb.Append("</runningTime>\n");

							sb.Append("</statistics>\n");

							//
							// Test Methods Block
							//
							sb.Append("<testMethods count=\"");
							sb.Append(C.Methods.Length);
							sb.Append("\">\n");

							foreach (Method m in C.Methods)
							{
								//
								// Test Method Block
								//
								description = HttpUtility.HtmlEncode(m.Description.Trim());
								if (!m.HasRun)
								{
									sb.Append("<testMethod name=\"");
									sb.Append(m.Name);
									sb.Append("\" hasRun=\"");
									sb.Append(m.HasRun);
									sb.Append("\" >\n");

									if (description.Length == 0)
									{
										sb.Append("<description />\n");
									}
									else
									{
										sb.Append("<description>");
										sb.Append(description);
										sb.Append("</description>\n");
									}

									sb.Append("</testMethod>\n");
								}
								else
								{
									//sb.Append("<!-- <testMethod name=\"\" > -->\n");

									sb.Append("<testMethod name=\"");
									sb.Append(m.Name);
									sb.Append("\" hasRun=\"");
									sb.Append(m.HasRun);
									sb.Append("\">\n");

									if (description.Length == 0)
									{
										sb.Append("<description />\n");
									}
									else
									{
										sb.Append("<description>");
										sb.Append(description);
										sb.Append("</description>\n");
									}

									//
									// Statistics Block
									//
									sb.Append("<statistics>\n");

									sb.Append("<passed>");
									sb.Append(m.TestRecord.Successes);
									sb.Append("</passed>\n");

									sb.Append("<failed>");
									sb.Append(m.TestRecord.Failures);
									sb.Append("</failed>\n");

									sb.Append("<total>");
									sb.Append((m.TestRecord.Failures + m.TestRecord.Successes));
									sb.Append("</total>\n");

									sb.Append("<runningTime>");
									sb.Append(m.RunningTime);
									sb.Append("</runningTime>\n");

									sb.Append("</statistics>\n");

									sb.Append("<tests count=\"");
									sb.Append(m.TestRecord.TestCount);
									sb.Append("\">\n");

									//sb.Append("<!-- <test order=\"1\" result=\"Passed|Failed\" ></test> -->\n");

									int count = m.TestRecord.TestObjectCount;
									for (int i=0; i < count; i++)
									{
										TestObject to = m.TestRecord.GetResult(i);
										if (to.TestObjectType == TestObjectTypes.TestResult)
										{
											TestResult tr = (TestResult)to;

											sb.Append("<test order=\"");
											sb.Append((i+1).ToString());
											sb.Append("\" result=\"");
											sb.Append((tr.Success ? "Passed" : "Failed"));
											sb.Append("\">");
											sb.Append(HttpUtility.HtmlEncode(tr.Description.Trim()));
											sb.Append("</test>\n");
										}
									}
									sb.Append("</tests>\n");

									// 
									// Warning: Update this calculation if we add more TestObject types 
									// than just result and comment.
									//
									int commentCount = m.TestRecord.TestObjectCount - m.TestRecord.TestCount;
									if (commentCount == 0)
									{
										sb.Append("<comments count=\"0\" />");
									}
									else
									{
										sb.Append("<comments count=\"");
										sb.Append(commentCount);
										sb.Append("\">\n");

										for (int i=0; i < count; i++)
										{
											TestObject to = m.TestRecord.GetResult(i);
											if (to.TestObjectType == TestObjectTypes.TestComment)
											{
												TestComment c = (TestComment)to;

												sb.Append("<comment order=\"");
												sb.Append((i+1).ToString());
												sb.Append("\">");
												sb.Append(HttpUtility.HtmlEncode(c.Comment));
												sb.Append("</comment>\n");
											}
										}
										sb.Append("</comments>\n");
									}
						

									AssociatedTestTracing trace = 
										traceListener.GetAssociationByName(m.ClassName + "." + m.Name);

									string [] traceMessages = null;
									bool nonEmptyTrace = false;
									if (trace != null)
									{
										traceMessages = trace.TraceMessages;
										count = traceMessages.Length;
										if (count > 0)
										{
											nonEmptyTrace = true;

											sb.Append("<traces count=\"");
											sb.Append(count);
											sb.Append("\" >\n");

											for (int i=0; i < count; i++)
											{
												string message = traceMessages[i];
												sb.Append("<trace order=\"");
												sb.Append((i+1).ToString());
												sb.Append("\">");
												sb.Append(HttpUtility.HtmlEncode(message));
												sb.Append("</trace>\n");
											}
											sb.Append("</traces>\n");
										}
									}
									if (!nonEmptyTrace)
									{
										sb.Append("<traces count=\"0\" />\n");
									}

									TestException [] exceptions = m.TestRecord.TestExceptions;
									foreach (TestException te in exceptions)
									{
										count++;
										sb.Append("<exceptionOccurred order=\"");
										sb.Append(count);
										sb.Append("\">\n");

										sb.Append("<registered>\n");

										sb.Append("<expected>");
										sb.Append(m.TestRecord.ExpectException);
										sb.Append("</expected>\n");

										if (m.TestRecord.ExpectException)
										{
											string message = m.TestRecord.ExpectedExceptionMessage.Trim();

											if (message.Length > 0)
											{
												sb.Append("<message>");
												sb.Append(HttpUtility.HtmlEncode(message));
												sb.Append("</message>\n");
											}
											else
											{
												sb.Append("<message />");
											}

											sb.Append("<consistent>");
											sb.Append(m.TestRecord.ConsistentException);
											sb.Append("</consistent>\n");

											sb.Append("<expectedType>");
											sb.Append(m.TestRecord.ExpectedExceptionType);
											sb.Append("</expectedType>\n");
										}

										sb.Append("</registered>\n");

										//
										// There are two cases: 
										//
										// If the exception comes from inside the test method, it gets
										// artifically wrapped in a TargetInvocationException. The first
										// case handles this and strips the added outer exception.
										//
										// If there is a problem with the test engine (among other things
										// inside HarnessIt) the there will be no TargetInvocationException
										// exception wrapper, so we need to report just that exception. 
										// That's the else clause.
										//
										if (te.Type.ExceptionType == "System.Reflection.TargetInvocationException")
										{
											sb.Append(BuildExceptionXml(te.InnerException));
										}
										else
										{
											sb.Append(BuildExceptionXml(te));
										}

										sb.Append("</exceptionOccurred>\n");
									}

									sb.Append("</testMethod>\n");
								} // if (hasRun) for method.
							}
							sb.Append("</testMethods>\n");
							sb.Append("</class>\n");
						} // if (hasRun) for class.
					}
					sb.Append("</classes>\n");
					sb.Append("</assembly>\n");
				} // if (hasRun) for assembly.
			}
			sb.Append("</assemblies>\n");
			sb.Append("</project>\n");
			sb.Append("</projects>\n");
			sb.Append("</solution>\n");

			string xmlText = sb.ToString().Replace("\n", "\r\n");

//			Debugging output:
//			StreamWriter sw = new StreamWriter("C:\\HarnessItTestLog.xml");
//			sw.Write(xmlText);
//			sw.Close();

			XmlDocument doc = new XmlDocument();
			doc.LoadXml(xmlText);

			return doc;
		}

		private static string GetIPAddress()
		{
			IPAddress [] addresses = Dns.GetHostEntry(Dns.GetHostName()).AddressList;

			StringBuilder sb = new StringBuilder();
			foreach (IPAddress ip in addresses)
			{
				sb.Append(ip.ToString());
				sb.Append(" ");
			}

			if (addresses.Length > 0)
			{
				return sb.ToString().Trim();
			}
			else
			{
				return "Unknown";
			}
		}

		private static string BuildExceptionXml(TestException e)
		{
			if (e == null)
			{
				return "";
			}

			StringBuilder sb = new StringBuilder(1024);

			sb.Append("<exception type=\"");
			sb.Append(e.Type.ExceptionType);
			sb.Append("\" source=\"");
			sb.Append(HttpUtility.HtmlEncode(e.Source));
			sb.Append("\">\n");

			sb.Append("<thread threadID=\"0x");
			sb.Append(e.ThreadID.ToString("X6"));
			sb.Append("\">\n");
			sb.Append("<name>");
			sb.Append(HttpUtility.HtmlEncode(e.ThreadName));
			sb.Append("</name>\n");
			sb.Append("</thread>\n");

			if (e.Message.Length > 0)
			{
				sb.Append("<message>");
				sb.Append(HttpUtility.HtmlEncode(e.Message));
				sb.Append("</message>\n");
			}
			else
			{
				sb.Append("<message />");
			}

			int length = 0;
			StackTraceEntry [] stack = null;

			if (e.StackTrace != "null")
			{
				StackTraceParser parser = new StackTraceParser(e.StackTrace);
				stack = parser.Entries;
				length = stack.Length;
			}

			if (length == 0)
			{
				sb.Append("<stackTrace />\n");
			}
			else
			{
				sb.Append("<stackTrace>\n");
				StackTraceEntry entry;
				for (int i=0; i < length; i++)
				{
					entry = stack[i];
					sb.Append("<entry order=\"");
					sb.Append((i+1));
					sb.Append("\" method=\"");
					sb.Append(HttpUtility.HtmlEncode(entry.Method));
					sb.Append("\" class=\"");
					sb.Append(entry.ClassName);
					if (entry.Line > 0)
					{
						sb.Append("\" line=\"");
						sb.Append(entry.Line);
					}
					if (entry.File.Length > 0)
					{
						sb.Append("\" file=\"");
						sb.Append(entry.File);
					}
					sb.Append("\" namespace=\"");
					sb.Append(entry.NameSpace);
					sb.Append("\" />\n");
				}
				sb.Append("</stackTrace>\n");
			}
			
			sb.Append("<toString>");
			sb.Append(HttpUtility.HtmlEncode(e.ToStringText));
			sb.Append("</toString>\n");

			sb.Append(BuildExceptionXml(e.InnerException));

			sb.Append("</exception>\n");

			return sb.ToString();
		}

	}
}
